#!/usr/bin/python
# Copyright (c) 2021 Contributors to the Eclipse Foundation
# 
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
# 
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# http://www.eclipse.org/legal/epl-2.0
# 
# SPDX-License-Identifier: EPL-2.0
#

#############################################################################################
### mqttbench helper classes for creating an mqttbench v5 client list file and message files 
### 
### To use this file add the following line to the top of your python script:
### from mqttbenchObjs import *
###
### Use classes below to instantiate objects that will be serialized to the mqttbench client list
### or message files.  See newClientList_ExX.py for examples.  
###
### messages = []
### clients = []
###
### client1 = MBClient(_id="Example1", version=MBCONSTANTS.MQTT_TCP5)
### clients.append(client1)
### MBCL.MBprintclients(clients)        # to dump contents of MBClient objects to stdout
###
### message1 = MBMessage("/home/dev/msgdir1/ExampleMsg1.json")
### messages.append(message1)
### MBCL.MBwriteMessageFiles(messages)  # to write message files
### MBCL.MBprintMessages(messages)      # to dump contents of MBMessage objects to stdout
###
#############################################################################################

import sys
import json
import random
import os
import copy
import base64

class MBCONSTANTS:
    MQTT_TCP3 = "TCP3"          # MQTT v3.1 over TCP
    MQTT_TCP4 = "TCP4"          # MQTT v3.1.1 over TCP
    MQTT_TCP5 = "TCP5"          # MQTT v5 over TCP
    MQTT_WS3 = "WS3"            # MQTT v3.1 over WebSockets
    MQTT_WS4 = "WS4"            # MQTT v3.1.1 over WebSockets
    MQTT_WS5 = "WS5"            # MQTT v5 over WebSockets
    
    MSG_FMT_BINARY = "application/octet-stream"
    MSG_FMT_TEXT = "text/plain"
    MSG_FMT_JSON = "application/json"

# In the classes below a description comment is provided next to each field.  The format of the description is as follows:
# # <type>: <MQTT Message(s)>: <MQTT v5 Specification section(s)> - <description> 

# MBUserProperty is an object used to store an MQTT v5 user property which may be added to the userProperties list in an MBMessage or MBClient object
# User properties are only valid for MQTT v5 (or later) clients.  User properties are sent on CONNECT or PUBLISH messages. 
class MBUserProperty:
    name = None                   # str: CONNECT/PUBLISH: 3.1.2.11.8/3.3.2.3.7 - name of the user property (user property names beginning with '@' are reserved by mqttbench, do not create user properties that begin with '@') 
    value = None                  # str: CONNECT/PUBLISH: 3.1.2.11.8/3.3.2.3.7 - value of the user property
    
    def __init__(self, name, value):
        if name.startswith("@"):
            raise Exception("MBUserProperty name: " + name + " , starts with '@', which is reserved by mqttbench for internal message properties")
        self.name = name
        self.value = value
        
    def __repr__(self):
        if not isinstance(self.name, str) or not isinstance(self.value, str):
            raise Exception("MBUserProperty name/value pair must both be strings, property name: " + str(self.name))
        return json.dumps(self.__dict__, sort_keys=True)

# MBMessage is an instance of a message object used by mqttbench.  This object class is used to construct message objects and write them to a 
# directory which can be later be processed by mqttbench at runtime. When printed this object is serialized to JSON and written to a file with
# the path specified by the path field of this object.  The mqttbench client list file contains a list of clients, each which may optionally specify
# a message directory path. The directory specified by the messageDirPath field of the MBClient object must contain the files generated by this helper
# script. Alternatively, mqttbench can generate basic binary messages of a specified size (command line param -s <min>-<max>)and WITHOUT any message properties.
#
# All publishing clients must have a message to send, i.e. -s command line parameter must be set OR the client must specify a message directory path) 
class MBMessage:
    #### Fields common to all MQTT protocol versions
    # Required
    path = None                 # str:      MQTTBENCH:     - name of the path to which the contents of this message object will be written to by this script. The messageDirPath directory 
                                #                            is where mqttbench will look for the message files at runtime. The message files generated by this script will need to be copied
                                #                            to the messageDirPath before mqttbench is started
    payload = None              # str:      PUBLISH: 3.3.3 - payload of the message to send. payload, payloadSizeBytes, and payloadFile are mutually exclusive. If the payload field is set, 
                                #                            the format is expected to be ASCII text (i.e. not binary)
    payloadSizeBytes = None     # int:      PUBLISH: 3.3.3 - size of the generated payload (bytes). If payloadSizeBytes is set, mqttbench will generate a binary payload of this size.
                                #                            alternatively you can use the command line parameter -s <min>-<max>, see "Getting Started Guide" for more information on the mqttbench command line
    payloadFile = None          # str:      PUBLISH: 3.3.3 - path to a file containing the contents of the payload.  the file will be read in binary mode as byte stream and payload format will be set to binary.
                                #                            The path of the payload file is relative to the working directory at runtime, it is NOT relative to the message directory path where this message JSON file is located.
    
    # Optional
    topic = None                # MBTopic:  PUBLISH: 3.3.3 - An MBTopic object maybe specified in the message object.  In which case it will override the default round robin selection of a topic from the 
                                #                            publishTopics list specified in the MBClient object.    
    
    #### Fields allowed for MQTT protocol version 5 or later
    contentType = None          # str:      PUBLISH: 3.3.2.3.9 - Format of the payload (e.g. application/json).  This field does NOT apply when using payloadSizeBytes.
    expiryIntervalSecs = 0      # int:      PUBLISH: 3.3.2.3.3 - Time in seconds for this message to live in broker subscription queue before removal
    userProperties = []         # list:     PUBLISH: 3.3.2.3.7 - a list of MBUserProperty objects (user specified properties) associated with the message (properties starting with '@' are reserved for mqttbench use)
    
    # Helper function to deserialize a list of dictionaries embedded in an object        
    def objdump(self, obj):
        return obj.__dict__
    
    def __init__(self, path):
        if path == None or len(path) == 0 or os.access(os.path.dirname(path), os.W_OK) == False:
            raise Exception("MBMessage was allocated with an invalid path: " + path + " . Specify a path that is both valid and writeable.")
        self.path = path
        self.userProperties = []
        
    def __repr__(self):
        payloadOpts=0
        if not self.payloadSizeBytes is None:
            payloadOpts+=1
        if not self.payload is None:
            payloadOpts+=1
        if not self.payloadFile is None:
            payloadOpts+=1
            
        if payloadOpts == 0:
            raise Exception("MBMessage with path " + self.path + " does not specify a payload, a non-zero payloadSizeBytes, or a payloadFile. You must set ONLY ONE of the three payload options.")
        if payloadOpts > 1:
            raise Exception("MBMessage with path " + self.path + " has a specified more than one payload option (non-zero payloadSizeBytes, payload, or payloadFile).  You must set ONLY ONE of the three payload options.")
        if not self.topic is None and not isinstance(self.topic, MBTopic):
            raise Exception("MBMessage with path " + self.path + " has specified a topic (\"" + str(self.topic) + "\"), but the value is not an instance of MBTopic object.  the topic field must be of type MBTopic.")
        if not self.payloadSizeBytes is None:
            if not self.contentType is None: 
                print("Warning: MBMessage with path " + self.path + " has specified payloadSizeBytes AND contentType, removing the contentType from the message (does not apply for binary messages)")
                del self.contentType     # is someone set contentType on a binary message, remove the content type
        if self.expiryIntervalSecs > 0xFFFFFFFF:
            raise Exception("MBMessage with path " + self.path + " has an expiryIntervalSecs larger than MAX_UINT, which is invalid.")
        if not self.userProperties is None and not len(self.userProperties) == 0 and not all(isinstance(x, MBUserProperty) for x in self.userProperties):
            raise Exception("MBMessage with path " + self.path + " has specified user properties which are NOT of object type MBUserProperty. All user properties, must be of type MBUserProperty")

        # Deep copy in order to remove path field from the output
        clone = copy.deepcopy(self)
        del clone.path
        if not clone.userProperties is None and len(clone.userProperties) == 0:
            del clone.userProperties
        if not clone.topic is None and not clone.topic.userProperties is None and len(clone.topic.userProperties) == 0:
            del clone.topic.userProperties
            
        return json.dumps(clone.__dict__, sort_keys=True, default=self.objdump, indent=2)

# MBTopic is a topic object which is added to either the publishTopics or subscriptions lists in the MBClient object.  It may also be added to an
# MBMessage object to override the default algorithm for how messages are published to topics in the publishTopics list of the MBClient object.  See
# the mqttbench Getting Started Guide for a description of the default algorithm for how messages are published to topics in the publishTopics list.
class MBTopic:
    #### Fields common to all MQTT protocol versions
    # Required    
    qos = 0                     # int:  PUBLISH/SUBSCRIBE:   3.3.1.2 - the Quality of Service governing message delivery and reliability on this topic. Default is 0.
    topicStr = None             # str:  PUBLISH/SUBSCRIBE:   3.3.2.1 - the topic string to publish or subscribe to (e.g. "/this/is/a/topic" or "/this/is/a/topic/with/a/wildcard/+", wildcards are not valid in PUBLISH, only SUBSCRIBE)
    
    # Optional
    retain = False              # bool: PUBLISH:   3.3.1.3 - indicates whether the retained flag is set on messages published to the topic. Publish side only. Default is false.
    
    #### Fields allowed for MQTT protocol version 5 or later
    responseTopicStr = None     # str:  PUBLISH:   3.3.2.3.5 - a topic to send a response to when the subscriber receives messages from this topic. Note, if responseTopicStr is specified at the message level (e.g. MBMessage) it overrides this field.
    correlationData = None      # str:  PUBLISH:   3.3.2.3.6 - data passed which can be used to correlate this message with (e.g. the client id)
    noLocal = None              # bool: SUBSCRIBE: 3.8.3.1   - if set to true, the server must not forward this message to a connection with a client ID equal to the client ID of the publishing connection
    retainAsPublished = None    # bool: SUBSCRIBE: 3.8.3.1   - if set to true, messages forwarded using this subscription keep the retain flag they were published with. if set to false, server should set retain flag to 0 on forwarded message.
    retainHandling = None       # int:  SUBSCRIBE: 3.8.3.1   - 0 = send retained message at time of subscribe, 1 = send only if subscription does not currently exist, 2 = do not send retained message at time of subscribe
    userProperties = []         # list: PUBLISH:   3.3.2.3.7 - a list of MBUserProperty objects (user specified properties) associated with the message (properties starting with '@' are reserved for mqttbench use)
    
    # Helper function to deserialize a list of dictionaries embedded in an object        
    def objdump(self, obj):
        return obj.__dict__
    
    def __init__(self, topic, qos=0):
        self.topicStr = topic
        self.qos = qos
        self.userProperties = []
        
    def __repr__(self):
        if self.topicStr is None or len(self.topicStr) == 0:
            raise Exception("MBTopic must have a non-zero length 'topic' field.")
        if not self.userProperties is None and not len(self.userProperties) == 0 and not all(isinstance(x, MBUserProperty) for x in self.userProperties):
            raise Exception("MBMessage with path " + self.path + " has specified user properties which are NOT of object type MBUserProperty. All user properties, must be of type MBUserProperty")
        if self.userProperties is None or len(self.userProperties) == 0:
            del self.userProperties
        if not isinstance(self.noLocal, bool) or not isinstance(self.retainAsPublished, bool):
            raise Exception("MBMessage with path " + self.path + " has specified a subscription with an invalid type for noLocal or retainAsPublished, these should be of type bool")
        return json.dumps(self.__dict__, sort_keys=True)        

# Last Will and Testament Message object
class MBLastWillMsg:
    #### Fields common to all MQTT protocol versions
    # Required
    topicStr = None             # str:  CONNECT: 3.1.3.3 - the topic string to publish the Last Will and Testament (LWT) message to
    payload = None              # str:  CONNECT: 3.1.3.4 - the contents of the LWT message to be published.  Only supports non-binary payload format for an LWT message.

    # Optional
    qos = 0                     # int:  CONNECT: 3.1.2.6 - the Quality of Service governing message delivery and reliability of the LWT message. Default is 0.
    retain = False              # bool: CONNECT: 3.1.2.7 - indicates whether the LWT message is to be retained once published. Default is 0, or false.
    
    #### Fields allowed for MQTT protocol version 5 or later
    #delayInterval = 0          # int:  CONNECT: 3.1.3.2.2 - currently not implemented on the server side (MessageSight or MSProxy). Determines elapsed time (in seconds) between end of connection and publication of the will message
    expiryIntervalSecs = 0      # int:  CONNECT: 3.1.3.2.4 - Time in seconds for this message to live on the broker before removal
    contentType = None          # str:  CONNECT: 3.1.3.2.5 - Indicates the content type of the will message (e.g. "application/json"), requires the payloadFormat indicator flag to be set to 1 
    responseTopicStr = None     # str:  CONNECT: 3.1.3.2.6 - a topic to send a response to when the subscriber receives messages from this topic.
    correlationData = None      # str:  CONNECT: 3.1.3.2.7 - data passed which can be used to correlate this message with (e.g. the client id)
    userProperties = []         # list: CONNECT: 3.1.3.2.8 - a list of MBUserProperty objects (user specified properties) to be sent with the Will Message
    
    def __init__(self, topic, payload, qos=0, retain=False):
        self.topicStr = topic
        self.payload = payload
        self.qos = qos
        self.retain = retain
        self.userProperties = []        # will be removed in MBClient __repr__ if < v5
        
    def __repr__(self):
        if not self.userProperties is None and not len(self.userProperties) == 0 and not all(isinstance(x, MBUserProperty) for x in self.userProperties):
            raise Exception("Will Message with topicStr: " + self.topicStr + " has specified user properties which are NOT of object type MBUserProperty. All user properties, must be of type MBUserProperty")
        
        return json.dumps(self.__dict__, sort_keys=True)  

class MBClient:
    #### Fields common to all MQTT protocol versions
    version = MBCONSTANTS.MQTT_TCP5     # str:  CONNECT:   3.1.2.2 - MQTT protocol version, see CONSTANTS (e.g. CONSTANTS.MQTT_TCP4 = MQTT 3.1.1 over TCP). Default is MQTT_TCP5.
    _id = ""                            # str:  CONNECT:   3.1.3.1 - MQTT client ID, required for MQTT version < 5, but optional for MQTT v5 clients (i.e. if you want the server to set the client id, then set _id to an empty string "")     
    publishTopics = []                  # list: PUBLISH:   3.3 - the list of topics that this client will publish messages to. Default is empty. Client must have at least 1 subscription OR 1 publish topic OR a message directory path containing a message file with a topic object.
    subscriptions = []                  # list: SUBSCRIBE: 3.8 - the list of topics that his client will subscribe to. Default is empty. Client must have at least 1 subscription OR publish topic OR a message directory path containing a message file with a topic object.

    # Optional
    username = None                     # str:  CONNECT:   3.1.3.5 - Username for authentication. No default.
    password = None                     # str:  CONNECT:   3.1.3.6 - Password for authentication. No default.
    dst = "127.0.0.1"                   # str:  TCP:       IP or DNS name of the messaging server to connect to. Default is 127.0.0.1
    dstPort = 1883                      # int:  TCP:       port of the messaging server to connect to. Default is 1883.
    useTLS = False                      # bool: TLS:       Use TLS security for connection to the messaging server. Default is non-tls.
    lingerTimeSecs = 0                  # int:  MQTTBENCH: time in seconds that the client should remain connected. 0 = client never initiates a connection close. Default is 0.
    lingerMsgsCount = 0                 # int:  MQTTBENCH: number of PUBLISH messages that the client sends before disconnecting. 0 = client never initiates a connection close. Default is 0.
                                        #                  if both lingerTimeSecs and lingerMsgCount are non-zero, whichever occurs first will initiate the connection close
    reconnectDelayUSecs = 0             # int:  MQTTBENCH: time in microseconds that the client should wait before reconnecting after it has closed the connection.  0 = reconnect without delay. Default is 0.
    clientCertPath = None               # str:  TLS:       fully qualified path to the location of the client certificate to send to the message broker. No client cert by default.
    clientKeyPath = None                # str:  TLS:       fully qualified path to the location of the client key to send to the message broker. No client cert by default.
    messageDirPath = None               # str:  PUBLISH:   3.3 - fully qualified path to the location of the directory containing the message files to be sent by this client. No client message dir by default.
                                        #                        the command line parameter -s <minSizeBytes>-<maxSizeBytes> is used to sent binary messages. Clients which are pure subscribers do not require a messageDirPath to be set
                                        #                        or the -s param, but clients which are publish require either a messageDirPath or the -s param.  The messageDirPath field takes precedence over the -s command line parameter.
    lastWillMsg = None                  # obj:  CONNECT:   3.1.3.2 - instance of MBLastWillMsg object. configuration of the Last Will and Testament(LWT) message to send if client remained disconnected or disconnected "uncleanly", i.e. w/o sending DISCONNECT message
    
    #### Fields allowed for MQTT protocol version 5 or later
    cleanStart = True                   # bool: CONNECT:   3.1.2.4     - Clean start flag, 1 = discard previous session state (if any) for this client. Default is 1.
    sessionExpiryIntervalSecs = 0       # int:  CONNECT:   3.1.2.11.2  - Session expiry interval (seconds), 0 = session ends at disconnect. Default is 0. MAX_INT (0xFFFFFFFF) means the session does not expire (can be overruled by connection policy on broker)
    recvMaximum = 0                     # int:  CONNECT:   3.1.2.11.3  - Used for flow control, tell broker not to send more than recvMaximum msgs (for QoS > 0) before receiving an ACK from this client (overrides global -mim command line parameter)
    maxPktSizeBytes = 0                 # int:  CONNECT:   3.1.2.11.4  - Set an upper limit on packet size (including headers) on packets sent by the broker to the client.
    topicAliasMaxIn = 0                 # int:  CONNECT:   3.1.2.11.5  - Set an upper limit on the number of topic aliases that this client will accept from the server
    topicAliasMaxOut = 0                # int:  CONNACK:   3.2.2.3.8   - Set an upper limit on the number of topic aliases that this client will send.  The server will ultimately determine the number, but this is used to indicate that mqttbench will send topic aliases
    requestResponseInfo = False         # bool: CONNECT:   3.1.2.11.6  - Ask server for a hint on how to construct response topics (for example, provide a topic tree root which is authorized only to this application).
    requestProblemInfo = False          # bool: CONNECT:   3.1.2.11.7  - Ask server to send a reason string or user property in addition to a reason code in case of failures
    userProperties = []                 # list: CONNECT:   3.1.2.11.8  - a list of MBUserProperty objects (user specified properties) to be sent with the CONNECT Message
    #authMethod = ""                    # str:  CONNECT:   3.1.2.11.9  - currently not implemented on the server side (MessageSight or MSProxy). Name of the authentication method used for authentication of this client
    #authData = ""                      # str:  CONNECT:   3.1.2.11.10 - currently not implemented on the server side (MessageSight or MSProxy). Binary data to send to the authentication service
    
    #### v3 and v3.1.1 fields
    cleanSession = True                 # bool: CONNECT:   Clean session flag, False = durable client. Default is True (i.e. non-durable)                 
    
    # Helper function to deserialize a list of dictionaries embedded in an object        
    def objdump(self, obj):
        return obj.__dict__
    
    def __init__(self, _id, version, dst="127.0.0.1", dstPort=1883, useTLS=False):
        self._id = _id
        self.version = version
        if not (version == MBCONSTANTS.MQTT_TCP5 or version == MBCONSTANTS.MQTT_WS5):
            if len(_id) == 0:
                raise Exception("Clients with version < v5 must provide a non-zero length client id")
            self.sessionExpiryIntervalSecs = None ; self.cleanStart = None ; self.recvMaximum = None ; self.maxPktSizeBytes = None
            self.topicAliasMaxIn = None ; self.topicAliasMaxOut = None ; self.requestResponseInfo = None ; self.requestProblemInfo = None
        self.dst = dst
        self.dstPort = int(dstPort)
        self.useTLS = bool(useTLS)
        self.publishTopics = []
        self.subscriptions = []
        self.userProperties = []
       
    def __repr__(self):
        # Perform some validation on the MBClient before returning the serialized form
        if(len(self.publishTopics) == 0):
            del self.publishTopics
            
        if(len(self.subscriptions) == 0):
            del self.subscriptions
            
        for topic in self.publishTopics:
            if len(topic.userProperties) == 0:
                del topic.userProperties
            
        for sub in self.subscriptions:
            if len(sub.userProperties) == 0:
                del sub.userProperties

        if not self.password is None:
            self.password = base64.b64encode(str(self.password))

        if not isinstance(self.useTLS, bool):
            raise Exception("Client with _id " + self._id + " useTLS field must be a boolean value. Current value: useTLS=" + str(self.useTLS))
        if not (self.version == MBCONSTANTS.MQTT_TCP5 or self.version == MBCONSTANTS.MQTT_WS5):
            del self.userProperties
            if not (self.sessionExpiryIntervalSecs is None and self.cleanStart is None and self.recvMaximum is None and
                    self.maxPktSizeBytes is None and self.topicAliasMaxIn is None and self.topicAliasMaxOut is None and self.requestResponseInfo is None and
                    self.requestProblemInfo is None):
                raise Exception("Client with _id " + self._id + " sessionExpiryIntervalSecs, cleanStart, recvMaximum, maxPktSizeBytes, topicAliasMaxIn, topicAliasMaxOut, requestResponseInfo, and requestProblemInfo are ONLY permitted for MQTT v5 or later clients")
            else:
                del self.sessionExpiryIntervalSecs ; del self.cleanStart ; del self.recvMaximum ; del self.maxPktSizeBytes 
                del self.topicAliasMaxIn; del self.topicAliasMaxOut; del self.requestResponseInfo ; del self.requestProblemInfo
            if len(self.publishTopics) > 0:
                for t in self.publishTopics:
                    if not t.responseTopicStr is None:
                        raise Exception("Client with _id " + self._id + " and publish topic " + t.topic + " response topics are only allowed for MQTT v5, or later, clients")
            if len(self.subscriptions) > 0:
                for s in self.subscriptions:
                    if not (s.noLocal is None and s.retainAsPublished is None and s.retainHandling is None):
                        raise Exception("Client with _id " + self._id + " and subscription " + s.topic + " subscriptions options (noLocal, retainAsPublished, and retainHandling) are only allowed for MQTT v5, or later, clients")
            if not self.lastWillMsg is None:
                if not isinstance(self.lastWillMsg, MBLastWillMsg):
                    raise Exception("Client with _id " + self._id + " lastWillMsg field not of type MBLastWillMsg, this field must be of type MBLastWillMsg")
                if not (self.lastWillMsg.correlationData is None and self.lastWillMsg.responseTopicStr is None and 
                        self.lastWillMsg.delayInterval is None and self.lastWillMsg.expiryInterval is None and 
                        self.lastWillMsg.payloadFormat is None and self.lastWillMsg.contentType is None):
                    raise Exception("Client with _id " + self._id + " will message properties like will delay interval, response topic, correlation data, message expiry," + 
                                    "content type, and payload format are ONLY permitted for MQTT v5 or later clients.")
                if len(self.lastWillMsg.userProperties) != 0:
                    raise Exception("Client with _id " + self._id + " will message user properties is not allowed before MQTT v5")
                else: 
                    del self.lastWillMsg.userProperties
        else:
            if not isinstance(self.cleanStart, bool):
                raise Exception("Client with _id " + self._id + " cleanStart field must be a boolean value. Current value: cleanStart=" + str(self.cleanStart))
            if self.userProperties is None or len(self.userProperties) == 0:
                del self.userProperties
            if not self.userProperties is None and not len(self.userProperties) == 0 and not all(isinstance(x, MBUserProperty) for x in self.userProperties):
                raise Exception("Client with _id " + self._id + " has specified user properties which are NOT of object type MBUserProperty. All user properties, must be of type MBUserProperty")
            if self.sessionExpiryIntervalSecs > 0xFFFFFFFF:
                raise Exception("Client with _id " + self._id + " sessionExpiryIntervalSecs value must be <= 0xFFFFFFFF. Current value: sessionExpiryIntervalSecs=" + str(self.sessionExpiryIntervalSecs))
 
        # return a json dump of the object
        return json.dumps(self.__dict__, sort_keys=True, default=self.objdump)

class MBCL:
    # Get Client ID from MBClient object
    @staticmethod
    def __getClientId(obj):
        return obj._id
    
    # Sort the list of clients by client ID
    @staticmethod
    def MBsortByClientId(clients):
        return sorted(clients, key=MBCL.__getClientId)
    
    # Shuffle the list of clients
    @staticmethod
    def MBshuffleClients(clients):
        return random.shuffle(clients);
    
    # Print the list of clients
    @staticmethod
    def MBprintClients(clients, fd=sys.stdout):
        fd.write("[\n")
        i=0
        for c in clients:
            if i != len(clients) -1:
                fd.write(str(c) + ",\n")
            else:
                fd.write(str(c) + "\n")
                
            i+=1
        fd.write("]\n") 

    # Print the list of messages to stdout
    @staticmethod
    def MBprintMessages(messages):
        fd = sys.stdout
        i=1
        for m in messages:    
            fd.write("MBMessage #" + str(i) + "\n")
            fd.write(str(m) + "\n\n")
            i+=1

    # Write the list of messages to their respective file paths (as specified by the path field of the object)
    @staticmethod
    def MBwriteMessageFiles(messages):
        for m in messages:
            try:
                fd = open(m.path, mode='w')
                fd.write(str(m))
                fd.close()
            except Exception as e:
                print("Failed to write mqttbench message file (path=" + m.path + "), exception: " + str(e))
                continue
    
